﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Data.Imaging;
using RayDen.Library.Entity.Interface;
using RayDen.RayEngine;
using RayDen.RayEngine.Core.Types;
using RayDen.RayEngine.Data;

namespace RayDen.MegaRay.Imaging
{
    public abstract class BaseFilm<TPixel> : IImagePlaneComponent
        where TPixel : IRgbConvertible 
    {

        protected static int GAMMA_TABLE_SIZE = 1024;

        public static int SampleBufferSize = 4096;

        public static float filterSize = 1.1f;

        protected bool SplatMode = GlobalConfiguration.Instance.Get("Film.SplatMode", false);

        public float linearToneMapValue = GlobalConfiguration.Instance.LinearToneMapValue;

        public bool Filter = GlobalConfiguration.Instance.FilterImage;

        public bool ToneMap = GlobalConfiguration.Instance.ToneMapping;

        protected Filter filter;

        protected FilterLUTs filterLUTs;

        public ToneMapParams toneMap;

        protected SamplePixel[] pixelData;

        protected FrameBuffer frameBuffer;

        public int Width;

        public int Height;

        public long statsTotalSampleCount;

        protected SampleBuffer Samples;

        protected float[] gammaTable;

        protected List<SampleBufferBase<TPixel>> sampleBuffers;
        protected LinkedList<SampleBufferBase<TPixel>> freeSampleBuffers;

        public BaseFilm(int width, int height)
        {
            this.Width = width;
            this.Height = height;
            this.Samples = new SampleBuffer((SampleBufferSize));
            this.frameBuffer = new FrameBuffer(width, height);
            this.InitGammaTable();
            this.pixelData = new SamplePixel[width * height];
            if (this.ToneMap)
            {
                this.toneMap =
                    //new Reinhard02ToneMapParams();
                    new LinearToneMapParams(linearToneMapValue);
            }

            if (this.Filter)
            {
                this.filter =
                    //new BoxFilter(1.2f, 1.2f);
                    new GaussianFilter(filterSize, filterSize);
                this.filterLUTs = new FilterLUTs(this.filter, 2);
            }

            this.sampleBuffers = new List<SampleBufferBase<TPixel>>();
            this.freeSampleBuffers = new LinkedList<SampleBufferBase<TPixel>>();
        }

        public void Reset()
        {
            lock (lclock)
            {
                this.Samples.Reset();
                this.frameBuffer.Clear();
            }
        }

        protected bool IsPreviewOver()
        {
            return true;
        }

        private readonly object lclock = new object();

        public virtual SampleBufferBase<TPixel> GetFreeSampleBuffer()
        {
            lock (lclock)
            {

                //          boost::mutex::scoped_lock lock(splatMutex);

                // Look for a free buffer
                if (freeSampleBuffers.Count > 0)
                {
                    var sb = freeSampleBuffers.First;
                    freeSampleBuffers.RemoveFirst();

                    sb.Value.Reset();
                    return sb.Value;
                }
                else
                {
                    // Need to allocate a new buffer
                    var sb = new SampleBufferBase<TPixel>(SampleBufferSize);
                    sampleBuffers.Add(sb);
                    return sb;
                }
            }
        }


        public void FreeSampleBuffer(SampleBufferBase<TPixel> sampleBuffer)
        {
            lock (lclock)
            {
                freeSampleBuffers.AddLast(sampleBuffer);
            }
        }

        public void SplatSampleBuffer(bool isPreviewOver, SampleBufferBase<TPixel> sampleBuffer)
        {
            lock (lclock)
            {
                var sbe = sampleBuffer;
                if (this.Filter)
                {
                    for (var i = 0; i < sampleBuffer.GetSampleCount(); ++i)
                    {
                        this.SplatFiltered(sampleBuffer.Samples[i]);
                    }
                }
                else
                {
                    for (var i = 0; i < sampleBuffer.GetSampleCount(); ++i)
                    {
                        var sampleElem = sbe.Samples[i];
                        int x = (int)sampleElem.X;
                        int y = (int)sampleElem.Y;
                        RgbSpectrum radiance;
                        sampleElem.Radiance.TransformToRgb(out radiance);
                        this.SplatRadiance(ref radiance, x, y, 1.0f);
                    }
                }
                //                statsTotalSampleTime += WallClockTime() - t;
                //statsTotalSamplesCount += sampleBuffer->GetSampleCount();

                freeSampleBuffers.AddLast(sampleBuffer);
            }
        }

        protected void SplatFiltered(SampleBufferElement<TPixel> sampleElem)
        {
            var dx = (sampleElem.X + 0.5f);
            var dy = (sampleElem.Y + 0.5f);
            var fLUT = this.filterLUTs.GetLUT(dx - (float)Math.Floor(sampleElem.X),
                                              dy - (float)Math.Floor(sampleElem.Y));
            var lut = fLUT.GetLUT();




            int x0 = MathLab.Ceil2UInt(dx - this.filter.xWidth);
            int x1 = x0 + fLUT.GetWidth();
            int y0 = MathLab.Ceil2UInt(dy - this.filter.yWidth);
            int y1 = y0 + fLUT.GetHeight();
            int lutOffset = 0;
            for (int iy = y0; iy < y1; ++iy)
            {
                if (iy < 0)
                {
                    lutOffset += fLUT.GetWidth();
                    continue;
                }
                else if (iy >= (this.Height))
                    break;

                for (int ix = x0; ix < x1; ++ix)
                {
                    float filterWt = lut[lutOffset++];

                    if ((ix < 0) || (ix >= (this.Width)))
                        continue;
                    RgbSpectrum radiance;
                    sampleElem.Radiance.TransformToRgb(out radiance);
                    this.SplatRadiance(ref radiance, ix, iy, filterWt);
                }
            }
        }

        public void SetGamma(float gamma = 2.2f)
        {
            float x = 0f;
            float dx = 1f / GAMMA_TABLE_SIZE;
            for (int i = 0; i < GAMMA_TABLE_SIZE; ++i, x += dx)
                this.gammaTable[i] = (float)Math.Pow(MathLab.Clamp(x, 0f, 1f), 1f / gamma);
        }

        internal void InitGammaTable()
        {
            float x = 0f;
            this.gammaTable = new float[GAMMA_TABLE_SIZE];
            float dx = 1f / GAMMA_TABLE_SIZE;
            for (int i = 0; i < GAMMA_TABLE_SIZE; ++i, x += dx)
                this.gammaTable[i] = (float)Math.Pow(MathLab.Clamp(x, 0f, 1f), 1f / 2.2f);
        }

        internal float Radiance2PixelFloat(float x)
        {
            // Very slow !
            //return powf(Clamp(x, 0.f, 1.f), 1.f / 2.2f);

            int index = Math.Min(
                MathLab.Floor2UInt(GAMMA_TABLE_SIZE * MathLab.Clamp(x, 0f, 1f)),
                GAMMA_TABLE_SIZE - 1);
            return this.gammaTable[index];
        }


        protected virtual void UpdateFrameBuffer()
        {
            var pixelCount = this.Width * this.Height;
            var sampledPixels = this.pixelData;
            var pix = new RgbSpectrum();
            if (!this.ToneMap)
            {
                //lock (lclock)
                for (int i = 0; i < pixelCount; ++i)
                {
                    if (this.SplatMode)
                    {
                        pix = new RgbSpectrum(this.Radiance2PixelFloat(pixelData[i].Radiance.c1),
                                              this.Radiance2PixelFloat(pixelData[i].Radiance.c2),
                                              this.Radiance2PixelFloat(pixelData[i].Radiance.c3));

                        this.AddPixel(i, ref  pixelData[i].Radiance, 1.0f);
                    }
                    else
                    {
                        ComputePixel(i);
                    }
                }
            }
            else if (this.toneMap != null)
            {
                switch (this.toneMap.GetToneMapType())
                {
                    case ToneMapType.TONEMAP_LINEAR:
                        var tmparam = (LinearToneMapParams)this.toneMap;
                        for (var j = 0; j < pixelCount; ++j)
                        {
                            if (this.SplatMode)
                            {
                                pix = new RgbSpectrum(
                                    this.Radiance2PixelFloat(sampledPixels[j].Radiance.c1),
                                    this.Radiance2PixelFloat(sampledPixels[j].Radiance.c2),
                                    this.Radiance2PixelFloat(sampledPixels[j].Radiance.c3));
                                this.SetPixel(j, ref pix);
                            }
                            else
                            {
                                if (!sampledPixels[j].Radiance.IsBlack())
                                {
                                    ComputePixel(j, tmparam.scale);
                                }

                            }
                        }
                        break;
                    case ToneMapType.TONEMAP_REINHARD02:
                        float alpha = .9f;
                        var tm = (Reinhard02ToneMapParams)this.toneMap;
                        float preScale = tm.preScale;
                        float postScale = tm.postScale;
                        float burn = tm.burn;
                        var sm = new SamplePixel[sampledPixels.Length];
                        float Ywa = 0f;
                        for (int ii = 0; ii < pixelCount; ++ii)
                        {
                            float weight = sampledPixels[ii].Weight;

                            if (weight > 0f)
                            {
                                float invWeight = 1f / weight;

                                RgbSpectrum rgb = sampledPixels[ii].Radiance * invWeight;

                                // Convert to XYZ color space
                                sm[ii].Radiance.c1 = 0.412453f * rgb.c1 + 0.357580f * rgb.c2 + 0.180423f * rgb.c3;
                                sm[ii].Radiance.c2 = 0.212671f * rgb.c1 + 0.715160f * rgb.c2 + 0.072169f * rgb.c3;
                                sm[ii].Radiance.c3 = 0.019334f * rgb.c1 + 0.119193f * rgb.c2 + 0.950227f * rgb.c3;

                                // var xyz = pixelsRadiance[i].XYZ();

                                Ywa += sm[ii].Radiance.c2;

                            }
                        }
                        Ywa /= pixelCount;

                        // Avoid division by zero
                        if (Ywa.NearEqual(0f))
                            Ywa = 1f;

                        float Yw = preScale * alpha * burn;
                        float invY2 = 1f / (Yw * Yw);
                        float pScale = postScale * preScale * alpha / Ywa;

                        for (int ii = 0; ii < pixelCount; ++ii)
                        {
                            RgbSpectrum xyz = sm[ii].Radiance;

                            float ys = xyz.c2;
                            xyz *= pScale * (1f + ys * invY2) / (1f + ys);

                            // Convert back to RGB color space
                            sampledPixels[ii].Radiance.c1 = 3.240479f * xyz.c1 - 1.537150f * xyz.c2 - 0.498535f * xyz.c3;
                            sampledPixels[ii].Radiance.c2 = -0.969256f * xyz.c1 + 1.875991f * xyz.c2 + 0.041556f * xyz.c3;
                            sampledPixels[ii].Radiance.c3 = 0.055648f * xyz.c1 - 0.204043f * xyz.c2 + 1.057311f * xyz.c3;

                            /*
                            this.frameBuffer.SetPixel(ii, this.Radiance2PixelFloat(sampledPixels[ii].radiance.c1),
                                                     this.Radiance2PixelFloat(sampledPixels[ii].radiance.c2),
                                                     this.Radiance2PixelFloat(sampledPixels[ii].radiance.c3));*/
                            pix = new RgbSpectrum(
                                this.Radiance2PixelFloat(sampledPixels[ii].Radiance.c1),
                                this.Radiance2PixelFloat(sampledPixels[ii].Radiance.c2),
                                this.Radiance2PixelFloat(sampledPixels[ii].Radiance.c3));
                            this.SetPixel(ii, ref pix);
                        }

                        break;
                }
            }


            //ClearPixelData();
        }

        protected virtual void ComputePixel(int i, float w = 1.0f)
        {
            RgbSpectrum.Gamma = true;

            float weight = pixelData[i].Weight;
            if (weight > 0f)
            {
                var pix = (pixelData[i].Radiance * (w / weight));
                pix.TransformToRgb();
                //(pixelData[i].radiance * (w / weight)).Apply(Radiance2PixelFloat);
                //new RgbSpectrum((sampledPixels[i].radiance/weight)).Transform();
                //s.Apply(Radiance2PixelFloat);

                this.SetPixel(i, ref pix);
            }
        }
        /*
        public static void SampleHitPoint(SampleBuffer sampleBuffer, PPMHitPoint hp, int photonTraced) {
            switch (hp.type) {
                case HitPointType.Constant: {
                        sampleBuffer.SplatSample(hp.scrX, hp.scrY, hp.throughput);
                        break;
                    }
                case HitPointType.Surface: {
                        if (hp.accumPhotonCount == 0) {
                            sampleBuffer.SplatSample(hp.scrX, hp.scrY, new RgbSpectrum());
                        }
                        else {
                            float k = 1.0f / (MathLab.M_PI * hp.photonRadius2 * photonTraced);
                            if (hp.accumDirectLightRadiance.IsBlack()) {
                                var rad = hp.accumReflectedFlux * k;
                                sampleBuffer.SplatSample(hp.scrX, hp.scrY, rad.ToRgb());
                            }
                            //else 
                            {
                                sampleBuffer.SplatSample(hp.scrX, hp.scrY, ((hp.accumReflectedFlux * k) + hp.accumDirectLightRadiance) / hp.accumPhotonCount);
                            }
                        }
                        break;
                    }
            }
        }
        */
        public  void UpdateScreenBuffer()
        {
            //lock (lclock)
            {
                //this.ClearPixelData();
                this.UpdateFrameBuffer();
                //frameBuffer.Clear();
            }
        }

        private void ClearPixelData()
        {
            this.pixelData = new SamplePixel[Width * Height];
        }

        public  float[] GetScreenBuffer()
        {
            return this.GetPixelData();
        }

        protected void SetPixel(int p0, ref RgbSpectrum pix)
        {
            this.frameBuffer.SetPixel(p0, ref pix);
        }

        protected virtual void SplatRadiance(ref RgbSpectrum radiance, int x, int y, float weight = 1.0f)
        {
            //int offset = x + y * width;
            this.AddPixel(x, y, ref radiance, weight);
        }

        protected void AddPixel(int x, int y, ref RgbSpectrum r, float w)
        {
            //var offset = x + (Height - 1 - y) * Width;
            var offset = x + (y) * this.Width;


            if (offset >= this.pixelData.Length)
                return;
            if (offset < 0)
                return;
            /*Parallel._AtomicAdd(ref pixels[offset].radiance.r, r.r);
            Parallel._AtomicAdd(ref pixels[offset].radiance.g, r.g);
            Parallel._AtomicAdd(ref pixels[offset].radiance.b, r.b);
            Parallel._AtomicAdd(ref pixels[offset].weight, w);
            */
            this.pixelData[offset].Radiance += r;
            this.pixelData[offset].Weight += w;//+= w;
            var i = r;
            //this.frameBuffer.SetPixel(offset, ref i);

        }
        protected void AddPixel(int offset, ref RgbSpectrum r, float w)
        {

            if (offset >= this.pixelData.Length)
                return;
            if (offset < 0)
                return;
            /*Parallel._AtomicAdd(ref pixels[offset].radiance.r, r.r);
            Parallel._AtomicAdd(ref pixels[offset].radiance.g, r.g);
            Parallel._AtomicAdd(ref pixels[offset].radiance.b, r.b);
            Parallel._AtomicAdd(ref pixels[offset].weight, w);
            */
            this.pixelData[offset].Radiance += r;
            this.pixelData[offset].Weight += w;//+= w;
            var i = this.pixelData[offset].Radiance;
            //this.frameBuffer.SetPixel(offset, ref i);
        }
        public void SavePNG(string fileName, string[] watermark)
        {
            lock (lclock)
            {
                this.UpdateFrameBuffer();
            }

            //RgbSpectrum.Gamma = false;
            using (Bitmap img = new Bitmap(this.Width, this.Height))
            {
                //BitmapData data = img.LockBits(new Rectangle(0, 0, Width, Height),
                //ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
                // unsafe {
                //byte* ptr = (byte*)data.Scan0.ToPointer();
                //int ImgOffset = data.Stride - Width * 3;
                var pixelData = this.GetPixelData();
                var nonz = Enumerable.Count<float>(pixelData, c => c > 0f);
                for (int y = 0; y < this.Height; y++)
                {
                    for (int x = 0; x < this.Width; x++
                         //,ptr += 3
                        )
                    {
                        var ii = nonz + 1;
                        var offset = (x + y * this.Width) * 3;

                        //                            int offset = 3 * (x + (height - y - 1) * width);
#if SPECTRUM
                        img.SetPixel(x, y, new RgbSpectrum(pixelData[offset], pixelData[offset + 1], pixelData[offset + 2]).ToColor());
#else

                        img.SetPixel(x, Height - y - 1, new RgbSpectrum(pixelData[offset], pixelData[offset + 1], pixelData[offset + 2]).ToColor());
#endif
                        //float weight = pixelWeights[x + (height - y - 1) * width];
                        //if (weight != 0)
                        //    weight = 1f/weight;
                        //var clr = (weight*pixelsRadiance[x + (height - y - 1)*width]).Transform();
                        //byte r = clr.R;
                        //byte g = clr.G;
                        //byte b = clr.B;

                        ////byte r = (byte)(255f * pixels[offset] + .5f);
                        ////byte g = (byte)(255f * pixels[offset + 1] + .5f);
                        ////byte b = (byte)(255f * pixels[offset + 2] + .5f);
                        //ptr[2] = r;
                        //ptr[1] = g;
                        //ptr[0] = b;
                    }
                    //    ptr += ImgOffset;
                }
                // }
                //    img.UnlockBits(data);
                //if (statsToRender != null)
                //{
                //    using (Graphics g = Graphics.FromImage(img))
                //    {
                //        using (var fn = new Font(FontFamily.GenericSansSerif, 9.5f))
                //        {
                //            for (int i = 0; i < statsToRender.Length; i++)
                //            {
                //                g.DrawString(statsToRender[i], fn, Brushes.White, 10, 550 + 12 * i);
                //            }
                //        }
                //    }
                //}
                ImageFactory.ApplyWaterMark(10, 400, img, watermark);
                img.Save(fileName);
            }
        }

        public float[] GetPixelData()
        {
            return this.frameBuffer.GetPixelData();
        }

        internal void SaveRaw(string fn)
        {

            ImageFactory.FreeImageSaveExrBitmap(fn, Width, Height, this.pixelData.Select(item => item.Weight > 0f ? item.Radiance / item.Weight : RgbSpectrum.ZeroSpectrum()).ToArray());

            //ImageFactory.FreeImageSaveExrBitmap(fn, Width, Height, this.frameBuffer.GetPixelData().ToSpectrumList().ToArray());
        }


    }
}